package com.atguigu.gmall.index.service.impl;

import ch.qos.logback.core.util.TimeUtil;
import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.bean.ResponseVo;
import com.atguigu.gmall.index.annotation.GmallCahe;
import com.atguigu.gmall.index.feign.GmallPmsClient;
import com.atguigu.gmall.index.service.IndexService;
import com.atguigu.gmall.index.utils.DistributedLock;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class IndexServiceImpl implements IndexService {
    @Autowired
    GmallPmsClient gmallPmsClient;

    @Autowired
    DistributedLock distributedLock;
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    RedissonClient redissonClient;
    private static final String KEY_PREFIX = "index:cates:";
    private static final String LOCK_PREFIX = "index:cate:lock:";
     private ThreadLocal<DistributedLock> threadLocal = new ThreadLocal<>();

    private String redisPrifix = "index:queryCategoryWithSub:";
    @Override
    public List<CategoryEntity> queryLvl1Categories() {
        //查询所有商品的一级分类
        ResponseVo<List<CategoryEntity>> categorys = gmallPmsClient.getCategorys("0");
        List<CategoryEntity> categoryEntityList = categorys.getData();

        return categoryEntityList;
    }

    //查询所有商品的二级分类及其子分类(使用redisson实现分布式锁)

    public List<CategoryEntity> queryLvl2CategoriesWithSub2(Long pid) {
        RLock lock = redissonClient.getLock("lock");
        lock.lock(5,TimeUnit.MINUTES);
        try {
            //在执行实际的业务逻辑前，需要查询缓存，如果缓存中没有，那么再查询mysql
            String value = redisTemplate.opsForValue().get(redisPrifix + pid);
            //如果查询出来的值为空，说明没有缓存，需要从mysql数据库中进行查询，并将查询出的结果存到redis
            if (StringUtils.isBlank(value)){
                ResponseVo<List<CategoryEntity>> listResponseVo = gmallPmsClient.queryCategoriesWithSub(pid);
                List<CategoryEntity> entityList = listResponseVo.getData();
                //  缓存穿透、大量的请求并发访问不存在的数据，redis去数据库中查询，造成mysql服务器宕机，解决方案：①不存在的数据也存起来 ②布隆过滤器
                String jsonString = JSON.toJSONString(entityList);
                if (CollectionUtils.isEmpty(entityList)){
                    redisTemplate.opsForValue().set(redisPrifix+pid,jsonString,5,TimeUnit.MINUTES);
                }
                //先保存的数据集合进行序列化，将其转换为json字符串,设置到redis中
                redisTemplate.opsForValue().set(redisPrifix+pid,jsonString,90,TimeUnit.DAYS);
                return entityList;
            }else {
                //如果查询到数据，则将查询到的数据变成list对象直接返回
                List<CategoryEntity> entityList = JSON.parseArray(value, CategoryEntity.class);
                return entityList;
            }
        } finally {
            lock.unlock();
        }
    }
    @GmallCahe(prefix = KEY_PREFIX, timeout = 129600, random = 14400, lockName = LOCK_PREFIX)
    public List<CategoryEntity> queryLvl2CategoriesWithSub(Long pid) {

        //如果查询出来的值为空，说明没有缓存，需要从mysql数据库中进行查询，并将查询出的结果存到redis
        ResponseVo<List<CategoryEntity>> listResponseVo = gmallPmsClient.queryCategoriesWithSub(pid);
        List<CategoryEntity> entityList = listResponseVo.getData();
        String jsonString = JSON.toJSONString(entityList);
        return entityList;
    }
    //查询所有商品的二级分类及其子分类(使用最原始的方法，没有考虑分布式的读并发问题)
    public List<CategoryEntity> queryLvl2CategoriesWithSub1(Long pid) {

        //在执行实际的业务逻辑前，需要查询缓存，如果缓存中没有，那么再查询mysql
        String value = redisTemplate.opsForValue().get(redisPrifix + pid);
        //如果查询出来的值为空，说明没有缓存，需要从mysql数据库中进行查询，并将查询出的结果存到redis
        if (StringUtils.isBlank(value)){
        ResponseVo<List<CategoryEntity>> listResponseVo = gmallPmsClient.queryCategoriesWithSub(pid);
        List<CategoryEntity> entityList = listResponseVo.getData();
            //  缓存穿透、大量的请求并发访问不存在的数据，redis去数据库中查询，造成mysql服务器宕机，解决方案：①不存在的数据也存起来 ②布隆过滤器
            String jsonString = JSON.toJSONString(entityList);
            if (CollectionUtils.isEmpty(entityList)){
                redisTemplate.opsForValue().set(redisPrifix+pid,jsonString,5,TimeUnit.MINUTES);
            }
            //先保存的数据集合进行序列化，将其转换为json字符串,设置到redis中
            redisTemplate.opsForValue().set(redisPrifix+pid,jsonString,90,TimeUnit.DAYS);
            return entityList;
        }else {
            //如果查询到数据，则将查询到的数据变成list对象直接返回
            List<CategoryEntity> entityList = JSON.parseArray(value, CategoryEntity.class);
            return entityList;
        }
    }



    /**
     *关于分布式锁的测试：1、使用场景：读多写少，使用redis的地方
     * 1、如何保证写的一致性问题
     * 双写模式：由于redis无法回滚，所以双写模式无法做到一致性
     * 失效模式：由于无法保证redis与mysql及时同步数据，做不到及时一致性
     * 双删模式：在写操作执行前删除
     * 2、缓存的常见问题：缓存穿透、缓存雪崩、缓存击穿
     *
     *
     */

    public  void pusherTest1() throws InterruptedException {
        //解决分布式并发问题，使用分布式锁
        //1、尝试获取锁，如果有人在用则重试，如果能获取到锁则获取
        String uuid = UUID.randomUUID().toString();
       //  如果加完锁之后redis服务器宕机,则该锁变成死锁,需要给锁设置过期时间,同时保证操作的原子性
            //  缓存雪崩 在redis中存在大量过期时间相同的数据，在同一时刻过期，此时该类数据大量请求并发访问，造成mysql服务器宕机解决方案：① 对redis中的数据设置不同的过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,30+new Random().nextInt(10),TimeUnit.SECONDS);

        if (lock){//没有锁，可以直接设置获取锁
        String json = redisTemplate.opsForValue().get("num");
        //如果num不等于空，说明redis里面有值
        if (!StringUtils.isBlank(json)){
            int i = Integer.parseInt(json);
            redisTemplate.opsForValue().set("num",String.valueOf(++i));
        }else {
            redisTemplate.opsForValue().set("num","1");
        }
        //代码逻辑执行完释放锁
            // 缓存击穿、热点key过期，大量请求并发访问，使用集群，构建分布式锁
        // 加入过期时间后，如果在执行删除操作之前锁已经过期了，需要判断删除的是否是自己的锁，防止误删;

            // 删除操作没有保证原子性，需要使用lua脚本
            String luaScript ="if redis.call('get',KEYS[1])==ARGV[1] " +
                    "then return redis.call('del',KEYS[1]) " +
                    "else return 0 end";
            RedisScript redisScript = new DefaultRedisScript(luaScript);
        redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

//            String lockjson = redisTemplate.opsForValue().get("lock");
//        if (StringUtils.equals(lockjson,uuid))
//            redisTemplate.delete("lock");
        }else {
            //有锁，说明有人占用，需要重试获取
            //需要让线程睡一会
         Thread.sleep(100);
            this.pusherTest();
        }

    }


    public void pusherTest() throws InterruptedException {

        String uuid = UUID.randomUUID().toString();
        Boolean lock = distributedLock.tryLock("lock", uuid, 30L);
        if (lock) {

                String json = redisTemplate.opsForValue().get("num");
                //如果num不等于空，说明redis里面有值
                if (!StringUtils.isBlank(json)) {
                    int i = Integer.parseInt(json);
                    redisTemplate.opsForValue().set("num", String.valueOf(++i));
                } else {
                    redisTemplate.opsForValue().set("num", "1");
//                    this.subTest(uuid);
                }
//                    TimeUnit.SECONDS.sleep(1000);
            threadLocal.set(distributedLock);
                distributedLock.unlock("lock", uuid);
        }
    }

    //可重入锁验证方法
    public void subTest(String uuid){
        this.distributedLock.tryLock("lock",uuid,30l);
        //业务方法
        this.distributedLock.unlock("lock",uuid);
    }
}
